import os, re, sys, time
import importlib
import waf_dynamo
import waflib.Task, waflib.TaskGen, waflib.Errors
from waflib.TaskGen import extension
import dlib
import google.protobuf.text_format
from threading import Lock
stderr_lock = Lock()
importlib_lock = Lock()

def create_simple_protoc_task(name, ext, compiled_ext, type, before, shell = True, color = 'PINK', proto_file = ''):
    def create(self, node):
        task = self.create_task(name)
        task.set_inputs(node)
        out = node.change_ext(compiled_ext)
        task.set_outputs(out)

    cmd = 'protoc --encode=%s -I ../src/gameobject/test -I ../src/gameobject/test/input -I ../proto -I ${DYNAMO_HOME}/share/proto -I ${DYNAMO_HOME}/ext/include %s < ${SRC} > ${TGT}' % (type, proto_file)
    waflib.Task.task_factory(name, cmd, before=before, shell=shell, color=color)
    extension(ext)(create)

create_simple_protoc_task('aresource', '.a_pb', '.a', 'TestGameObjectDDF.AResource', before='c cxx', proto_file = '../src/gameobject/test/component/test_gameobject_component_ddf.proto')
create_simple_protoc_task('bresource', '.b_pb', '.b', 'TestGameObjectDDF.BResource', before='c cxx', proto_file = '../src/gameobject/test/component/test_gameobject_component_ddf.proto')
create_simple_protoc_task('cresource', '.c_pb', '.c', 'TestGameObjectDDF.CResource', before='c cxx', proto_file = '../src/gameobject/test/component/test_gameobject_component_ddf.proto')
create_simple_protoc_task('message_target', '.mt_pb', '.mt', 'TestGameObjectDDF.MessageTarget', before='c cxx', proto_file = '../src/gameobject/test/message/test_gameobject_message_ddf.proto')
create_simple_protoc_task('input_target', '.it_pb', '.it', 'TestGameObjectDDF.InputTarget', before='c cxx', proto_file = '../src/gameobject/test/input/test_gameobject_input_ddf.proto')
create_simple_protoc_task('reload_target', '.rt_pb', '.rt', 'TestGameObjectDDF.ReloadTarget', before='c cxx', proto_file = '../src/gameobject/test/reload/test_gameobject_reload_ddf.proto')
create_simple_protoc_task('no_user_data_res', '.no_user_data', '.no_user_datac', 'TestGameObjectDDF.NoUserDataResource', before='c cxx', proto_file = '../src/gameobject/test/props/test_gameobject_props_ddf.proto')


def clear_caches():
    importlib_lock.acquire()
    try:
        importlib.invalidate_caches() # make sure any modules created after the processes started will be found
    finally:
        importlib_lock.release()

def transform_properties(properties, out_properties):
    clear_caches()
    import gameobject_ddf_pb2

    for property in properties:
        entry = None
        if property.type == gameobject_ddf_pb2.PROPERTY_TYPE_NUMBER:
            entry = out_properties.number_entries.add()
            entry.index = len(out_properties.float_values)
            out_properties.float_values.append(float(property.value))
        elif property.type == gameobject_ddf_pb2.PROPERTY_TYPE_HASH:
            entry = out_properties.hash_entries.add()
            entry.index = len(out_properties.hash_values)
            out_properties.hash_values.append(dlib.dmHashBuffer64(property.value))
        elif property.type == gameobject_ddf_pb2.PROPERTY_TYPE_URL:
            entry = out_properties.url_entries.add()
            entry.index = len(out_properties.string_values)
            out_properties.string_values.append(property.value)
        elif property.type == gameobject_ddf_pb2.PROPERTY_TYPE_VECTOR3:
            entry = out_properties.vector3_entries.add()
            entry.index = len(out_properties.float_values)
            out_properties.float_values.extend([float(v.strip()) for v in property.value.split(',')])
        elif property.type == gameobject_ddf_pb2.PROPERTY_TYPE_VECTOR4:
            entry = out_properties.vector4_entries.add()
            entry.index = len(out_properties.float_values)
            out_properties.float_values.extend([float(v.strip()) for v in property.value.split(',')])
        elif property.type == gameobject_ddf_pb2.PROPERTY_TYPE_QUAT:
            entry = out_properties.quat_entries.add()
            entry.index = len(out_properties.float_values)
            out_properties.float_values.extend([float(v.strip()) for v in property.value.split(',')])
        elif property.type == gameobject_ddf_pb2.PROPERTY_TYPE_BOOLEAN:
            entry = out_properties.bool_entries.add()
            entry.index = len(out_properties.float_values)
            out_properties.float_values.append(float(property.value == "true"))
        else:
            raise Exception("Invalid type")
        entry.key = property.id
        entry.id = dlib.dmHashBuffer64(property.id)

def transform_collection(task, msg):
    for instance in msg.instances:
        for comp_props in instance.component_properties:
            transform_properties(comp_props.properties, comp_props.property_decls)
        instance.id = "/" + instance.id

    return msg

def merge_collections(msg, sub_msg, sub_id):
    for inst in sub_msg.instances:
        new_inst = msg.instances.add()
        new_inst.MergeFrom(inst)
        new_inst.id = sub_id + "/" + inst.id

def read_collection(path):
    clear_caches()
    import gameobject_ddf_pb2

    msg = gameobject_ddf_pb2.CollectionDesc()
    with open(path, 'rb') as in_f:
        google.protobuf.text_format.Merge(in_f.read(), msg)

    for coll_inst in msg.collection_instances:
        sub_msg = read_collection(os.path.dirname(path) + coll_inst.collection)
        merge_collections(msg, sub_msg, coll_inst.id)
    msg.collection_instances.__delitem__(slice(0, len(msg.collection_instances)))

    return msg

def compile_collection(task):
    try:
        src_path = task.inputs[0].srcpath()
        msg = read_collection(src_path)
        msg = transform_collection(task, msg)

        with open(task.outputs[0].abspath(), 'wb') as out_f:
            out_f.write(msg.SerializeToString())

        return 0
    except (google.protobuf.text_format.ParseError, google.protobuf.message.EncodeError, Exception) as e:
        stderr_lock.acquire()
        try:
            print ('%s: %s' % (src_path, str(e)), file=sys.stderr)
        finally:
            stderr_lock.release()
        return 1

task = waflib.Task.task_factory('collection',
                                func    = compile_collection,
                                color   = 'RED',
                                after='proto_gen_py',
                                before='c cxx')

@extension('.collection')
def collectionfile(self, node):
    task = self.create_task('collection')
    task.set_inputs(node)

    out = node.change_ext('.collectionc')
    task.set_outputs([out])

def transform_gameobject(task, msg):
    for component in msg.components:
        transform_properties(component.properties, component.property_decls)
    return msg

def compile_go(task):
    clear_caches()
    import gameobject_ddf_pb2

    try:
        msg = gameobject_ddf_pb2.PrototypeDesc()
        with open(task.inputs[0].srcpath(), 'rb') as in_f:
            google.protobuf.text_format.Merge(in_f.read(), msg)
        msg = transform_gameobject(task, msg)
        with open(task.outputs[0].abspath(), 'wb') as out_f:
            out_f.write(msg.SerializeToString())

        return 0
    except (google.protobuf.text_format.ParseError, google.protobuf.message.EncodeError, Exception) as e:
        stderr_lock.acquire()
        try:
            print ('%s: %s' % (task.inputs[0].srcpath(), str(e)), file=sys.stderr)
        finally:
            stderr_lock.release()
        return 1

task = waflib.Task.task_factory('gameobject',
                                func    = compile_go,
                                color   = 'RED',
                                after=['proto_gen_py_package','proto_gen_py'],
                                before='c cxx')

@extension('.go_pb')
def gofile(self, node):
    task = self.create_task('gameobject')
    task.set_inputs(node)

    out = node.change_ext('.goc')
    task.set_outputs([out])

from io import StringIO
def strip_single_lua_comments(str):
    str = str.replace("\r", "");
    sb = StringIO()
    for line in str.split('\n'):
        lineTrimmed = line.strip()
        # Strip single line comments but preserve "pure" multi-line comments
        # Note that ---[[ is a single line comment
        # You can enable a block in Lua by adding a hyphen, e.g.
        #
        # ---[[
        # The block is enabled
        # --]]
        #

        if not lineTrimmed.startswith("--") or lineTrimmed.startswith("--[[") or lineTrimmed.startswith("--]]"):
            sb.write(line)
        sb.write("\n")
    return sb.getvalue()

def strip_props(str):
    rp_prop = re.compile(r"go.property\(\"(.*?)\",\s*(.*?)\)$")
    str = str.replace("\r", "");
    sb = StringIO()
    for line in str.split('\n'):
        lineTrimmed = line.strip()
        # Strip property declarations
        if not rp_prop.match(lineTrimmed):
            sb.write(line)
        sb.write("\n")
    return sb.getvalue()

def scan_lua(str):
    str = strip_single_lua_comments(str)
    ptr = re.compile(r'--\[\[.*?--\]\]', re.MULTILINE | re.DOTALL)
    # NOTE: We don't preserve line-numbers
    # '' could be replaced with a function
    str = ptr.sub('', str)

    modules = []
    props = {}
    rp1 = re.compile(r"require\s*?\"(.*?)\"$")
    rp2 = re.compile(r"require\s*?\(\s*?\"(.*?)\"\s*?\)$")
    rp_prop = re.compile(r"go.property\(\"(.*?)\",\s*(.*?)\)$")
    for line in str.split('\n'):
        line = line.strip()
        m1 = rp1.match(line)
        m2 = rp2.match(line)
        if m1:
            modules.append(m1.group(1))
        elif m2:
            modules.append(m2.group(1))
        m_prop = rp_prop.match(line)
        if m_prop:
            props[m_prop.group(1)] = m_prop.group(2).strip()
    return modules, props

def parse_properties(props):
    def add_elements(entry, id, elements):
        for e in elements:
            key = "%s.%s" % (id, e)
            entry.element_ids.append(dlib.dmHashBuffer64(key))

    clear_caches()
    import properties_ddf_pb2

    declarations = properties_ddf_pb2.PropertyDeclarations()
    if props:
        # http://docs.python.org/dev/library/re.html#simulating-scanf
        rp_num = re.compile(r"[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?")
        rp_hash = re.compile(r"hash\(\"(.*?)\"\)")
        rp_url = re.compile(r"msg\.url\((\"(.*?)\")?\)")
        rp_vec3 = re.compile(r"vmath\.vector3\(((.*?),(.*?),(.*?)|)\)")
        rp_vec4 = re.compile(r"vmath\.vector4\(((.*?),(.*?),(.*?),(.*?)|)\)")
        rp_quat = re.compile(r"vmath\.quat\(((.*?),(.*?),(.*?),(.*?)|)\)")
        rp_bool = re.compile(r"(true|false)")
        rp_resource = re.compile(r"resource\.(.*?)\((\"(.*?)\")?\)")
        for k,v in props.items():
            m_num = rp_num.match(v)
            m_hash = rp_hash.match(v)
            m_url = rp_url.match(v)
            m_vec3 = rp_vec3.match(v)
            m_vec4 = rp_vec4.match(v)
            m_quat = rp_quat.match(v)
            m_bool = rp_bool.match(v)
            m_resource = rp_resource.match(v)
            entry = None
            if m_num:
                entry = declarations.number_entries.add()
                entry.index = len(declarations.float_values)
                num = float(m_num.group(0))
                declarations.float_values.append(num)
            elif m_hash:
                entry = declarations.hash_entries.add()
                entry.index = len(declarations.hash_values)
                hash = dlib.dmHashBuffer64(m_hash.group(1))
                declarations.hash_values.append(hash)
            elif m_url:
                entry = declarations.url_entries.add()
                entry.index = len(declarations.string_values)
                if (m_url.group(2) and len(m_url.group(2)) > 0):
                    url = m_url.group(2)
                else:
                    url = ""
                declarations.string_values.append(url)
            elif m_vec3:
                entry = declarations.vector3_entries.add()
                entry.index = len(declarations.float_values)
                add_elements(entry, k, ("x", "y", "z"))
                if (m_vec3.group(2)):
                    vec3 = (float(m_vec3.group(2)), float(m_vec3.group(3)), float(m_vec3.group(4)))
                else:
                    vec3 = (0, 0, 0)
                declarations.float_values.extend(vec3)
            elif m_vec4:
                entry = declarations.vector4_entries.add()
                entry.index = len(declarations.float_values)
                add_elements(entry, k, ("x", "y", "z", "w"))
                if (m_vec4.group(2)):
                    vec4 = (float(m_vec4.group(2)), float(m_vec4.group(3)), float(m_vec4.group(4)), float(m_vec4.group(5)))
                else:
                    vec4 = (0, 0, 0, 0)
                declarations.float_values.extend(vec4)
            elif m_quat:
                entry = declarations.quat_entries.add()
                entry.index = len(declarations.float_values)
                add_elements(entry, k, ("x", "y", "z", "w"))
                if (m_quat.group(2)):
                    quat = (float(m_quat.group(2)), float(m_quat.group(3)), float(m_quat.group(4)), float(m_quat.group(5)))
                else:
                    quat = (0, 0, 0, 1)
                declarations.float_values.extend(quat)
            elif m_bool:
                entry = declarations.bool_entries.add()
                entry.index = len(declarations.float_values)
                if m_bool.group(0) == 'true': num = 1
                else: num = 0
                declarations.float_values.append(num)
            elif m_resource:
                entry = declarations.hash_entries.add()
                entry.index = len(declarations.hash_values)
                resource = m_resource.group(3) or ''
                hash = dlib.dmHashBuffer64(resource)
                declarations.hash_values.append(hash)
            else:
                # TODO Handle error?
                print("%s has unknown format: \"%s\"" % (k,v))
                pass
            entry.key = k
            entry.id = dlib.dmHashBuffer64(k)

    return declarations

def compile_lua(task):
    clear_caches()
    import lua_ddf_pb2

    with open(task.inputs[0].srcpath(), 'r') as in_f:
        script = in_f.read()
        modules, props = scan_lua(script)
        script = strip_props(script)
        lua_module = lua_ddf_pb2.LuaModule()
        lua_module.source.script = script.encode()
        lua_module.source.filename = task.inputs[0].srcpath()
        for m in modules:
            module_file = "/%s.lua" % m.replace(".", "/")
            lua_module.modules.append(m)
            lua_module.resources.append(module_file + 'c')
        lua_module.properties.CopyFrom(parse_properties(props))
        with open(task.outputs[0].abspath(), 'wb') as out_f:
            out_f.write(lua_module.SerializeToString())

    return 0

task = waflib.Task.task_factory('luascript',
                                func    = compile_lua,
                                color   = 'PINK')

@extension('.script')
def script_file(self, node):
    obj_ext = '.scriptc'
    task = self.create_task('luascript')
    task.set_inputs(node)
    out = node.change_ext(obj_ext)
    task.set_outputs(out)

@extension('.lua')
def script_file(self, node):
    obj_ext = '.luac'
    task = self.create_task('luascript')
    task.set_inputs(node)
    out = node.change_ext(obj_ext)
    task.set_outputs(out)

def build(bld):
    def new_test(dir, exts = ['.cpp', '.proto', '.go_pb', '.script']):
        exported_symbols = ['ResourceTypeGameObject',
                            'ResourceTypeCollection',
                            'ResourceTypeScript',
                            'ResourceTypeLua',
                            'ResourceTypeAnim',
                            'ResourceProviderFile',
                            'ComponentTypeScript',
                            'ComponentTypeAnim']
        test_task_gen = bld.program(features = 'cxx cprogram test',
                                    includes = '../../../src . .. ../../../proto',
                                    source = ['test_main.cpp'] + bld.path.ant_glob('%s/*' % (dir), incl=exts),
                                    exported_symbols = exported_symbols,
                                    use = 'TESTMAIN APP SOCKET PLATFORM_THREAD RESOURCE DDF SCRIPT GRAPHICS_NULL PLATFORM_NULL LUA EXTENSION DLIB PLATFORM_NULL PROFILE_NULL RIG HID gameobject',
                                    web_libs = ['library_sys.js', 'library_script.js'],
                                    proto_gen_py = True,
                                    target = 'test_gameobject_%s' % dir)

    new_test('anim')
    new_test('bones', exts = ['.cpp', '.a_pb', '.go_pb', '.script'])
    new_test('collection', exts = ['.cpp', '.go_pb', '.script', '.collection', '.a_pb'])
    new_test('spawn_delete', exts = ['.cpp', '.proto', '.go_pb', '.script', '.a_pb'])
    new_test('component', exts = ['.cpp', '.proto', '.go_pb', '.script', '.a_pb', '.b_pb', '.c_pb'])
    new_test('delete')
    new_test('factory', exts = ['.cpp', '.a_pb', '.go_pb', '.script'])
    new_test('hierarchy')
    new_test('id')
    new_test('input', exts = ['.go_pb', '.script', '.cpp', '.proto', '.it_pb'])
    new_test('message', exts = ['.go_pb', '.script', '.cpp', '.proto', '.mt_pb'])
    new_test('props')
    new_test('reload', exts = ['.go_pb', '.script', '.cpp', '.proto', '.rt_pb'])
    new_test('script')
    new_test('lua')
